#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>

#include <iostream>
#include <unordered_map>

struct thing {
	int member_variable = 5;

	double member_function() const {
		return member_variable / 2.0;
	}
};

#define TEMPLATE_AUTO(x) decltype(x), x

// cheap storage: in reality, you'd need to find a
// better way of handling this.
// or not! It's up to you.
static std::unordered_map<sol::string_view, sol::object>
     thing_function_associations;
static std::unordered_map<sol::string_view, sol::object>
     thing_variable_associations;

void register_thing_type(sol::state& lua) {
	thing_variable_associations.emplace_hint(
	     thing_variable_associations.cend(),
	     "member_variable",
	     sol::object(lua.lua_state(),
	          sol::in_place,
	          &sol::c_call<TEMPLATE_AUTO(
	               &thing::member_variable)>));
	thing_function_associations.emplace_hint(
	     thing_function_associations.cend(),
	     "member_function",
	     sol::object(lua.lua_state(),
	          sol::in_place,
	          &sol::c_call<TEMPLATE_AUTO(
	               &thing::member_function)>));

	struct call_handler {
		static int lookup_function(lua_State* L) {
			sol::stack_object source(L, 1);
			sol::stack_object key(L, 2);
			if (!source.is<thing>()) {
				return luaL_error(L,
				     "given an incorrect object for this "
				     "call");
			}
			sol::optional<sol::string_view> maybe_svkey
			     = key.as<sol::optional<sol::string_view>>();
			if (maybe_svkey) {
				{
					// functions are different from
					// variables functions, when obtain with
					// the syntax obj.f, obj.f(), and
					// obj:f() must return the function
					// itself so we just push it realy into
					// our target
					auto it
					     = thing_function_associations.find(
					          *maybe_svkey);
					if (it
					     != thing_function_associations
					             .cend()) {
						return it->second.push(L);
					}
				}
				{
					// variables are different than funtions
					// when someone does `obj.a`, they
					// expect this __index call (this lookup
					// function) to return to them the value
					// itself they're seeing so we call out
					// lua_CFunction that we serialized
					// earlier
					auto it
					     = thing_variable_associations.find(
					          *maybe_svkey);
					if (it
					     != thing_variable_associations
					             .cend()) {
						// note that calls generated by
						// sol2 for member variables expect
						// the stack ordering to be 2(, 3,
						// ..., n) -- value(s) 1 -- source
						// so we destroy the key on the
						// stack
						sol::stack::remove(L, 2, 1);
						lua_CFunction cf
						     = it->second
						            .as<lua_CFunction>();
						return cf(L);
					}
				}
			}
			return sol::stack::push(L, sol::lua_nil);
		}

		static int insertion_function(lua_State* L) {
			sol::stack_object source(L, 1);
			sol::stack_object key(L, 2);
			sol::stack_object value(L, 3);
			if (!source.is<thing>()) {
				return luaL_error(L,
				     "given an incorrect object for this "
				     "call");
			}
			// write to member variables, etc. etc...
			sol::optional<sol::string_view> maybe_svkey
			     = key.as<sol::optional<sol::string_view>>();
			if (maybe_svkey) {
				{
					// variables are different than funtions
					// when someone does `obj.a`, they
					// expect this __index call (this lookup
					// function) to return to them the value
					// itself they're seeing so we call out
					// lua_CFunction that we serialized
					// earlier
					auto it
					     = thing_variable_associations.find(
					          *maybe_svkey);
					if (it
					     != thing_variable_associations
					             .cend()) {
						// note that calls generated by
						// sol2 for member variables expect
						// the stack ordering to be 2(, 3,
						// ..., n) -- value(s) 1 -- source
						// so we remove the key value
						sol::stack::remove(L, 2, 1);
						lua_CFunction cf
						     = it->second
						            .as<lua_CFunction>();
						return cf(L);
					}
					else {
						// write to member variable, maybe
						// override function if your class
						// allows for it?
						(void)value;
					}
				}
				// exercise for reader:
				// how do you override functions on the
				// metatable with proper syntax, but error
				// when the type is an "instance" object?
			}
			return 0;
		}
	};

	lua.new_usertype<thing>("thing");
	sol::table metatable = lua["thing"];

	metatable[sol::meta_method::index]
	     = &call_handler::lookup_function;
	metatable[sol::meta_method::new_index]
	     = &call_handler::insertion_function;
}

void unregister_thing_type(sol::state&) {
	thing_function_associations.clear();
	thing_variable_associations.clear();
}

int main() {

	std::cout << "=== metatable with custom-built (static) "
	             "handling ==="
	          << std::endl;


	sol::state lua;
	lua.open_libraries(sol::lib::base);

	// register custom type + storage
	register_thing_type(lua);

	lua.script(R"(t = thing.new() 
		print(t.member_variable)
		print(t:member_function())
		t.member_variable = 24
		print(t.member_variable)
		print(t:member_function())
	)");

	// clear storage
	unregister_thing_type(lua);

	std::cout << std::endl;

	return 0;
}
